use std::collections::{HashSet, VecDeque};

type Coord = (i32, i32, i32);

fn parse_input() -> Vec<Coord> {
    include_str!("../input.txt")
        .lines()
        .map(|line| -> Coord {
            let mut iter = line.split(',');
            let x = iter.next().unwrap().parse().unwrap();
            let y = iter.next().unwrap().parse().unwrap();
            let z = iter.next().unwrap().parse().unwrap();
            (x, y, z)
        })
        .collect()
}

fn adjacent_coords(coord: &Coord) -> [Coord; 6] {
    [
        (coord.0 - 1, coord.1, coord.2),
        (coord.0 + 1, coord.1, coord.2),
        (coord.0, coord.1 - 1, coord.2),
        (coord.0, coord.1 + 1, coord.2),
        (coord.0, coord.1, coord.2 - 1),
        (coord.0, coord.1, coord.2 + 1),
    ]
} 

fn part1() {
    let mut coord_set = HashSet::new();
    let mut surface_count = 0;

    for coord in parse_input() {
        let hide_count = adjacent_coords(&coord)
            .into_iter()
            .filter(|adj_coord| coord_set.contains(adj_coord))
            .count();

        surface_count -= hide_count;
        surface_count += 6 - hide_count;

        coord_set.insert(coord);
    }

    println!("Part 1: {}", surface_count);
}

fn part2() {
    let coords = parse_input();

    let min_x = coords.iter().map(|coord| coord.0).min().unwrap() - 1;
    let max_x = coords.iter().map(|coord| coord.0).max().unwrap() + 1;
    let length = (max_x - min_x + 1) as usize;

    let min_y = coords.iter().map(|coord| coord.1).min().unwrap() - 1;
    let max_y = coords.iter().map(|coord| coord.1).max().unwrap() + 1;
    let width = (max_y - min_y + 1) as usize;

    let min_z = coords.iter().map(|coord| coord.2).min().unwrap() - 1;
    let max_z = coords.iter().map(|coord| coord.2).max().unwrap() + 1;
    let height = (max_z - min_z + 1) as usize;

    struct Blocks {
        blocks: Vec<Vec<Vec<bool>>>, 
        min_x: i32, 
        max_x: i32,
        min_y: i32,
        max_y: i32, 
        min_z: i32,
        max_z: i32,
    }

    impl Blocks {
        fn new(blocks: Vec<Vec<Vec<bool>>>, min_x: i32, max_x: i32, min_y: i32, max_y: i32, min_z: i32, max_z: i32) -> Self {
            Self {blocks, min_x, max_x, min_y, max_y, min_z, max_z}
        }

        fn get_block(&self, coord: &Coord) -> bool {
            self.blocks[(coord.0 - self.min_x) as usize][(coord.1 - self.min_y) as usize][(coord.2 - self.min_z) as usize]
        }

        fn set_block(&mut self, coord: &Coord, value: bool) {
            self.blocks[(coord.0 - self.min_x) as usize][(coord.1 - self.min_y) as usize][(coord.2 - self.min_z) as usize] = value;
        }

        fn in_range(&self, coord: &Coord) -> bool {
            (coord.0 >= self.min_x && coord.0 <= self.max_x) &&
            (coord.1 >= self.min_y && coord.1 <= self.max_y) &&
            (coord.2 >= self.min_z && coord.2 <= self.max_z)
        }
    }

    let mut blocks = Blocks::new(vec![vec![vec![false; height]; width]; length], min_x, max_x, min_y, max_y, min_z, max_z);
    for coord in coords {
        blocks.set_block(&coord, true);
    }
    let blocks = blocks;

    let mut queue = VecDeque::<Coord>::new();
    let mut coord_set = HashSet::<Coord>::new();
    let mut surface_count = 0;

    queue.push_back((min_x, min_y, min_z));

    while let Some(coord) = queue.pop_front() {
        // Coord must be air
        for adj_coord in adjacent_coords(&coord) {
            if !blocks.in_range(&adj_coord) {
                continue;
            }

            if blocks.get_block(&adj_coord) {
                // Is a lava surface
                surface_count += 1;
            } else if !coord_set.contains(&adj_coord) {
                queue.push_back(adj_coord);
                coord_set.insert(adj_coord);
            }
        }
    }

    println!("Part 2: {}", surface_count);

}

fn main() {
    part1();
    part2();
}
